/*******************************************************************************
 * Copyright 2013 Mindaugas Idzelis
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package incontrol.rcon.protocol;

import incontrol.bf3.packets.EventFactory;
import incontrol.bf3.packets.PacketFactory;
import incontrol.bf3.packets.ResponseFactory;
import incontrol.bf3.protocol.events.EventType;
import incontrol.bf3.protocol.events.ServerEvent;
import incontrol.bf3.protocol.requests.LogonRequest;
import incontrol.bf3.protocol.requests.BF3Request;
import incontrol.bf3.protocol.requests.SendEventsRequest;
import incontrol.bf3.protocol.responses.Response;
import incontrol.bf3.state.ServerState;
import incontrol.server.EventManager;
import incontrol.server.StateUpdater;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicLong;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.google.common.collect.Maps;
import com.google.common.collect.Queues;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.google.common.util.concurrent.Uninterruptibles;

public class RCONClient {

	private Log packetLog = LogFactory.getLog(getClass().getName() + ".packet");

	// private boolean connected = false;
	// private boolean loggedIn = false;

	public AtomicLong sequence = new AtomicLong();

	private Socket socket;
	private InputStream in;
	private OutputStream out;

	private Map<BF3Request, SettableFuture<Response>> pendingResponses = Maps
			.newConcurrentMap();
	private Map<Long, BF3Request> pendingRequests = Maps.newConcurrentMap();

	private BlockingQueue<Packet> packetsToSend = Queues
			.newLinkedBlockingDeque();
	private EventFactory factory = new EventFactory();
	private ResponseFactory responseFactory = new ResponseFactory();

	private PacketFactory packetFactory;

	private StateUpdater updater;

	private EventManager eventManager;

	public RCONClient() {
		packetFactory = new PacketFactory();
	}

	public void login(String password) {
		Future<Response> send = send(new LogonRequest(password));
		Response loginResponse = Futures.getUnchecked(send);
		if (!loginResponse.isOK()) {
			throw new RuntimeException(loginResponse.getError());
		}
		LogFactory.getLog(getClass()).info("Login successful");

		send(new SendEventsRequest(true));

	}

	public ListenableFuture<Response> send(BF3Request request) {
		SettableFuture<Response> response = SettableFuture.create();
		Packet packet = packetFactory.createPacket(request,
				sequence.incrementAndGet());
		pendingResponses.put(request, response);
		pendingRequests.put(packet.getSequence(), request);
		Uninterruptibles.putUninterruptibly(packetsToSend, packet);
		return response;
	}

	public void connect(String host, int port) throws IOException {
		socket = new Socket(host, port);
		// boolean connected = socket.isConnected();
		out = socket.getOutputStream();
		in = socket.getInputStream();
		Thread read = new Thread() {
			public void run() {
				while (true) {
					try {
						Packet serverPacket = Packet.read(in);

						packetLog.trace("Received: " + serverPacket);
						// Packets are odd. Client/Server is from the point of
						// view of the BF3 server, and clients are Players.
						// The admin interface is designated as "Server"
						if (serverPacket.getType() == Type.RESPONSE) {
							long sequence = serverPacket.getSequence();
							BF3Request request = pendingRequests
									.remove(sequence);
							if (request != null) {
								SettableFuture<Response> futureResponse = pendingResponses
										.remove(request);
								if (futureResponse != null) {
									Response response = responseFactory.create(
											request, serverPacket);
									LogFactory.getLog(response.getClass())
											.trace("Received: " + response);
									futureResponse.set(response);
								}
							}
						} else {
							ServerEvent event = dispatchServerPacket(serverPacket);
							if (event.getEventType() != EventType.UNKNOWN) {
								packetsToSend.add(new Packet(Origin.CLIENT,
										Type.RESPONSE, serverPacket
												.getSequence(), "OK"));
							}
						}
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			};
		};
		read.start();
		Thread write = new Thread() {
			public void run() {
				while (true) {
					try {
						Packet toSend = Uninterruptibles
								.takeUninterruptibly(packetsToSend);
						if (toSend != null) {
							packetLog.trace("Sending: " + toSend.toString());
							if (toSend.getType() == Type.REQUEST) {
								BF3Request request = pendingRequests.get(toSend
										.getSequence());
								LogFactory.getLog(request.getClass()).trace(
										"Sending: " + request);
							}

							toSend.write(out);
							out.flush();

						}

					} catch (Exception e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
			};
		};
		write.start();

	}

	public void init(ServerState state, StateUpdater updater,
			EventManager eventManager) {
		this.updater = updater;
		this.eventManager = eventManager;
	}

	protected ServerEvent dispatchServerPacket(Packet serverPacket) {
		ServerEvent event = factory.create(serverPacket);
		LogFactory.getLog(event.getClass()).trace("Dispatching: " + event);
		updater.internalPost(event);
		eventManager.sendEvent(event);
		return event;
	}

	private void disconnect() {
		try {
			out.close();
		} catch (IOException e) {

		}
		out = null;
		try {
			in.close();
		} catch (IOException e) {

		}
		in = null;
		try {
			socket.close();
		} catch (IOException e) {

		}
		sequence.set(0);
		socket = null;
		// connected = false;
		// loggedIn = false;
	}

}
